﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace OMap {
    /// <summary>
	/// The main mapping class for mapping the data of one object into another one.
    /// </summary>
    public class OMap {

		#region Private Fields
		private readonly Dictionary<Type,Dictionary<Type,IObjectMapper>> _mapperCache = new Dictionary<Type, Dictionary<Type, IObjectMapper>>();
    	#endregion

		#region Public Properties
		/// <summary>
        /// Defines if existing value in the target value should be overwritten if they are none default values.
        /// E.g. if in the target is null the null will be always overwritten, but if there is already a value
        /// it won't be set unless this property was set to true.
        /// </summary>
        /// <remarks>
        /// Please note that if you are using collections and if the collection have already been initialized
        /// (value is not null) and do not have any values inside the collection this value wont be overwritten by the source value if this property was
        /// not set to true.
        /// </remarks>
        public bool OverwriteTarget {
            get; set;
        }


		/// <summary>
		/// Enables the creation of reverse mapping information
		/// </summary>
		public bool AutoReverseMap {
			get;
			set;
		}

		#endregion

		#region Private/Internal Helper Methods
		/// <summary>
		/// Gets a mapper object for the passed type information.
		/// </summary>
		/// <param name="sourceType">The source mapping type.</param>
		/// <param name="targetType">The target mapping type.</param>
		/// <param name="overwriteIfNotDefault">If set to <c>true</c> values in the target object will be overwritten even if there is already a value in the target object.</param>
		/// <param name="throwException">Throw an exception if the target type could not be resolved.</param>
		/// <returns></returns>
        internal IObjectMapper GetMapper(Type sourceType,Type targetType, bool overwriteIfNotDefault,bool throwException) {
            
            if (!_mapperCache.ContainsKey(sourceType)) {
                _mapperCache[sourceType] = new Dictionary<Type, IObjectMapper>(100);
            }

            //the following mapping retrieval will implement polymorphic mapping
            //in a list it is not possible to determine the correct target type
            //because the target type will be determined by retrieving the type information
            //of the generic list parameter (in case it is a generic list) - to enable
            //polymorphic mapping the user must define the mapping for a polymorphic type before
            //by adding manually a new entry into the mappercache

            //first return most detailed mapping hit 
            //this means sourcetype and targettype does match exactly
            if (targetType != null && _mapperCache[sourceType].ContainsKey(targetType)) {
                return _mapperCache[sourceType][targetType];
            }
			
			if(_mapperCache[sourceType].Count > 0 ) {
                //return the first matching entry for the source type
                //this could be a predefined entry for example
                KeyValuePair<Type, IObjectMapper> firstEntry = _mapperCache[sourceType].First();
                if(firstEntry.Value != null) {
                    return firstEntry.Value;
                }
            }

			if(targetType == null)
			{
				if (throwException)
				{
					throw new Exception("Invalid Mapping - targetType can not be resolved");
				}
				return null;
			}
            //if no mapper was found a new one must be created
            AddMap(sourceType, targetType, overwriteIfNotDefault,false);

            return _mapperCache[sourceType][targetType];
            
        }
		/// <summary>
		/// Adds the passed mapping information to the mapper cache.
		/// </summary>
		/// <param name="sourceType">Type of the source.</param>
		/// <param name="targetType">Type of the target.</param>
		/// <param name="overwriteIfNotDefault">if set to <c>true</c> [overwrite if not default].</param>
		/// <param name="isReverseMap">Determines if this method was called to add the reverse mapping information for the passed types.</param>
        private void AddMap(Type sourceType, Type targetType, bool overwriteIfNotDefault,bool isReverseMap) {
            

			IObjectMapper newObjectMapper = null;
            //if it is a collection - create a collection mapper
            if (IsCollection(sourceType,targetType) ) {
                newObjectMapper = new CollectionMapper(sourceType, targetType, overwriteIfNotDefault,this);                   
            } else {
                //otherwise create normal vaule by value mapper
                newObjectMapper =  new ElementMapper(sourceType, targetType, overwriteIfNotDefault,this);                 
            }

			this.AddMap(newObjectMapper);

            if(!isReverseMap && AutoReverseMap) {
                AddMap(targetType,sourceType,overwriteIfNotDefault,true);
            }
        }



		/// <summary>
		/// Determines whether the specified source type is a collection.
		/// </summary>
		/// <param name="sourceType">Type of the source.</param>
		/// <param name="targetType">Type of the target.</param>
		/// <returns>
		/// 	<c>true</c> if the specified source type is collection; otherwise, <c>false</c>.
		/// </returns>
		internal static bool IsCollection(Type sourceType,Type targetType) {
			bool targetOk = false;
			bool sourceOk = false;

			if (targetType.IsGenericType && typeof(IList<>).IsAssignableFrom(targetType.GetGenericTypeDefinition()) ||
				   typeof(IList).IsAssignableFrom(targetType)) {
				targetOk = true;
			} else {
				foreach (Type interfaceType in targetType.GetInterfaces()) {
					if (interfaceType.IsGenericType && typeof (IList<>).IsAssignableFrom(interfaceType.GetGenericTypeDefinition()) ||
					    typeof (IList).IsAssignableFrom(interfaceType)) {
						targetOk = true;
						break;
					}
				}
			}

			if (typeof(IEnumerable).IsAssignableFrom(sourceType)) {
				sourceOk = true;
			}else{
				foreach (Type interfaceType in targetType.GetInterfaces()) {
					if (typeof (IEnumerable).IsAssignableFrom(interfaceType)) {
						sourceOk = true;
						break;
					}
				}
			}

			return targetOk && sourceOk;
		}

		#endregion

		/// <summary>
		/// Adds a new mapping info. This Method should be used for adding some additional type information
		/// to enable polymorphic list mapping. E.g: You have a generic list typed to Class TestA
		/// and you have filled the list with instances of Test A and a sublcass of TestA called TestASub.
		/// If you want to map the list to another list that was typed to TestAA and you also have a TestAASub that should be mapped to TestASub.
		/// The mapper needs to know that instances of TestAA should be mapped to TestAASub.
		/// </summary>
		/// <remarks>
		/// In future releases I plan to add some new naming conventions to gues a polymorphic type match.
		/// </remarks>
		/// <param name="sourceType">The sourceType that should be mapped to the <paramref name="targetType"/></param>
		/// <param name="targetType">The target of the <paramref name="sourceType"/></param>
		/// <param name="overwriteTarget">If you set this parameter to <c>TRUE</c> existing values in the target object will be 
		/// overwritten.</param>
		public void AddMap(Type sourceType, Type targetType, bool overwriteTarget) {
            AddMap(sourceType,targetType,overwriteTarget,false);
        }

		/// <summary>
		/// Adds or Updates the passed mapping object to the collection of mappers.
		/// </summary>
		/// <param name="mapper">The mapper to add.</param>
		public void AddMap(IObjectMapper mapper) {

			if(!mapper.IsInitialized)
			{
				mapper.Initialize(this);
			}

			if (!_mapperCache.ContainsKey(mapper.SourceType)) {
				_mapperCache[mapper.SourceType] = new Dictionary<Type, IObjectMapper>(100);
			}
			_mapperCache[mapper.SourceType][mapper.TargetType] = mapper;
		}


		/// <summary>
		/// Map all data from the source object into a new target object.
		/// </summary>
		/// <typeparam name="TSource">Defines the type of the source object.</typeparam>
		/// <typeparam name="TTarget">Defines the type of the target object.</typeparam>
		/// <param name="source">The source object that holds all the data that should be mapped.</param>
		/// <returns></returns>
        public TTarget MapTo<TSource,TTarget>(TSource source) {
            var referenceDict = new Dictionary<object, object>();
			IObjectMapper mapper = GetMapper(source.GetType(), typeof (TTarget), OverwriteTarget,true);
			mapper.CalledBy = null;
            return (TTarget) mapper.MapTo(source,referenceDict);
        }

		/// <summary>
		/// Map all data from the source object into the passed target object.
		/// </summary>
		/// <typeparam name="TSource">Defines the type of the source object.</typeparam>
		/// <typeparam name="TTarget">Defines the type of the target object.</typeparam>
		/// <param name="source">The source object that holds all the data that should be mapped.</param>
		/// <param name="target">The target object into which all data should be mapped.</param>
		/// <returns></returns>
        public TTarget MapTo<TSource, TTarget>(TSource source,TTarget target) {
			var referenceDict = new Dictionary<object, object>();
			IObjectMapper mapper = GetMapper(typeof (TSource), typeof (TTarget), OverwriteTarget,true);
			mapper.CalledBy = null;
            return (TTarget) mapper.MapTo(source,target,referenceDict);
        }
    }
}
